﻿namespace NJQuery {
	using System;
	using System.ComponentModel;
	using System.Globalization;

	/// <summary>
	/// Defines an aspect ratio.
	/// </summary>
	[TypeConverter(typeof(AspectRatioConverter))]
	public struct AspectRatio : IEquatable<AspectRatio>, IComplexProperty {


		#region Static Readonly Fields: Standard values

		/// <summary>
		/// Instructs jQuery UI not to constrain the aspect ratio.
		/// </summary>
		public static readonly AspectRatio Unconstrained = new AspectRatio();

		/// <summary>
		/// Instructs jQuery UI to constrain using the existing aspect ratio.
		/// </summary>
		public static readonly AspectRatio Current = new AspectRatio(true);

		/// <summary>
		/// Instructs jQuery UI to constrain as a square.
		/// </summary>
		public static readonly AspectRatio Square = new AspectRatio(1F);

		#endregion


		#region Static Methods

		/// <summary>
		/// Converts a string representation to its <see cref="AspectRatio"/> equivalent.
		/// </summary>
		/// <param name="text">A string containing the value to convert.</param>
		/// <param name="result">The parsed value when the conversion succeeds, or a default instance otherwise.</param>
		/// <returns>Indicates whether the conversion succeeded.</returns>
		public static bool TryParse(string text, out AspectRatio result) {

			if(string.IsNullOrEmpty(text)) {
				result = Unconstrained;
				return true;
			}

			bool boolValue;
			if(bool.TryParse(text, out boolValue)) {
				result = new AspectRatio(boolValue);
				return true;
			}

			float floatValue;
			if(float.TryParse(text, out floatValue)) {
				result = new AspectRatio(floatValue);
				return true;
			}

			string[] splitText = text.Split('/');
			if(splitText.Length == 2) {
				int width; int height;
				if(int.TryParse(splitText[0].Trim(), out width) && int.TryParse(splitText[1].Trim(), out height)) {
					result = new AspectRatio(width, height);
					return true;
				}
			}

			result = Unconstrained;
			return false;
		}

		#endregion


		#region Fields

		private readonly float ratio;

		#endregion


		#region Constructors

		/// <summary>
		/// Initializes a new <see cref="AspectRatio"/> value using specified behavior.
		/// </summary>
		/// <param name="useCurrent">A value of true will constrain using the current aspect ratio while false will not constrain.</param>
		public AspectRatio(bool useCurrent) {
			ratio = useCurrent ? float.NaN : 0F;
		}

		/// <summary>
		/// Initializes a new <see cref="AspectRatio"/> value using a specific value.
		/// </summary>
		/// <param name="ratio">The aspect ratio used to constrain.</param>
		/// <exception cref="ArgumentOutOfRangeException">Ratio value must be a non-negative real number.</exception>
		public AspectRatio(float ratio) {
			if(ratio <= 0 || float.IsInfinity(ratio) || float.IsNaN(ratio))
				throw new ArgumentOutOfRangeException("ratio", "Value must be a non-negative real number.");

			this.ratio = ratio;
		}

		/// <summary>
		/// Initializes a new <see cref="AspectRatio"/> value using a specific value.
		/// </summary>
		/// <param name="width">The width portion of the ratio used to constrain.</param>
		/// <param name="height">The height portion of the ratio used to constrain.</param>
		public AspectRatio(int width, int height) : this((float)width / (float)height) { }

		#endregion


		#region Properties

		/// <summary>
		/// If applicable, the specific ratio used to constrain.
		/// </summary>
		public float? Ratio {
			get { return ratio == 0F || float.IsNaN(ratio) ? (float?)null : ratio; }
		}

		/// <summary>
		/// Whether or not the current aspect ratio should be used to constrain.
		/// </summary>
		public bool UseCurrent {
			get { return float.IsNaN(ratio); }
		}

		/// <summary>
		/// Whether or not the aspect ratio is constrained.
		/// </summary>
		public bool IsConstrained {
			get { return ratio != 0F; }
		}

		#endregion


		#region Methods: Object management

		/// <summary>
		/// Determines whether or not the specified object is equal to the current <see cref="AspectRatio"/> value.
		/// </summary>
		/// <param name="obj">The <see cref="Object"/> to compare.</param>
		/// <returns>Whether or not the provided object is equal.</returns>
		public override bool Equals(object obj) {
			if(obj is AspectRatio) return this == (AspectRatio)obj;
			return false;
		}

		/// <summary>
		/// Determines whether or not the specified AspectRatio is equal to the current <see cref="AspectRatio"/> value.
		/// </summary>
		/// <param name="other">The <see cref="AspectRatio"/> to compare.</param>
		/// <returns>Whether or not the provided <see cref="AspectRatio"/> value is equal.</returns>
		public bool Equals(AspectRatio other) {
			return this == other;
		}

		/// <summary>
		/// Serves as a hash function for <see cref="AspectRatio"/>.
		/// </summary>
		/// <returns>The hash value associated with the contained data.</returns>
		public override int GetHashCode() {
			return ratio.GetHashCode();
		}

		/// <summary>
		/// Returns a <see cref="String"/> that represents the current <see cref="AspectRatio"/> value.
		/// </summary>
		/// <returns>The string representation.</returns>
		public override string ToString() {
			if(Ratio.HasValue) return ratio.ToString(CultureInfo.InvariantCulture);
			return IsConstrained.ToString(CultureInfo.InvariantCulture);
		}

		/// <summary>
		/// Returns a literal JavaScript value used to represent the server value on the client.
		/// </summary>
		/// <returns>The client script value.</returns>
		public string ToClientSource() {
			if(!IsConstrained) return false.ToClientSource();
			if(UseCurrent) return true.ToClientSource();
			return ratio.ToClientSource();
		}

		#endregion


		#region Operators

		/// <summary>
		/// Compares the equality of two <see cref="AspectRatio"/> values.
		/// </summary>
		/// <param name="value0">The first value.</param>
		/// <param name="value1">The second value.</param>
		/// <returns>Whether or not the two values are equal.</returns>
		public static bool operator ==(AspectRatio value0, AspectRatio value1) {
			return value0.ratio == value1.ratio;
		}

		/// <summary>
		/// Compares the inequality of two <see cref="AspectRatio"/> values.
		/// </summary>
		/// <param name="value0">The first value.</param>
		/// <param name="value1">The second value.</param>
		/// <returns>Whether or not the two values are distinct.</returns>
		public static bool operator !=(AspectRatio value0, AspectRatio value1) {
			return value0.ratio != value1.ratio;
		}

		#endregion


	}

}